在之前的文章中有提出了當重複的東西一再出現的問題,當時並沒有特別的去談一些理論和解決方法,但在後面幾天小編開始慢慢的置入 SOLID 中的
今天想來開箱另外一個設計程式時的介面隔離原則,談談透過介面的隔離來確保高內聚性 (Cohesion) 和低耦合性 (Coupling),使程式碼易於理解、擴充和維護,一起來看看有沒有機會解決上次的問題吧。
讓我們複習一下之前提到的情境描述,假設我們正在開發一個角色扮演遊戲 (RPG) 的程式,其中有不同類型的玩家,包括基本玩家和進階玩家,每個玩家類型都有自己的檢查條件,重構的目標是希望確保程式碼易於擴充,以應對未來可能新增的玩家類型。
// 拆開
// 初始版本只有基本角色屬性
const player1 = {
name: "玩家1",
level: 1,
health: 100,
damage: 10,
};
// 一段時間後,遊戲需要更多的角色屬性
const player2 = {
name: "玩家2",
level: 5,
health: 150,
damage: 15,
agility: 20, // 新需求:敏捷度
inventory: ["劍", "盾"], // 新需求:背包
};
// 合起來
function processPlayerData(
name,
level,
health,
damage,
agility,
inventory,
statusCode
) {
if (
health > 10 &&
(name === "foo" || damage < 5) &&
(name !== "bar" || agility > 20)
) {
// ...
}
// 非常長的函數內容...
if (statusCode === 20100) {
// ...
}
if (statusCode === 20101) {
// ...
}
}
首先定義了 Player 的介面,包含了所有玩家類型都需要具備的通用屬性和方法。
但由於角色的不同,改善的方向應該是把角色分開,這樣每個角色都只會用到自己需要的屬性和方法。
createPlayer
函數實現了 Player 介面,確保了基本玩家包含了通用的屬性遵循了 Player 介面createAdvancedPlayer
函數擴充 Player 介面,確保了進階玩家不僅遵循了 Player 介面也包含了進階玩家特有的屬性// 定義一個函數,用於建立玩家物件
function createPlayer(playerData) {
return {
...playerData,
};
}
// 建立玩家物件
const player1 = createPlayer({
name: "玩家1",
level: 1,
health: 100,
damage: 10,
});
// 新需求:更多的角色屬性
function createAdvancedPlayer(advancedPlayerData) {
return {
...createPlayer(advancedPlayerData),
};
}
// 建立進階玩家物件
const player2 = createAdvancedPlayer({
name: "玩家2",
level: 5,
health: 150,
damage: 15,
agility: 20,
inventory: ["劍", "盾"],
});
// 重新設計函數,只接受角色物件作為參數
function processPlayerData(player) {
const { name, health, damage, agility } = player;
if (
health > 10 &&
(name === "foo" || damage < 5) &&
(name !== "bar" || agility > 20)
) {
// ...
}
// 非常長的函數內容...
if (statusCode === 20100) {
// ...
}
if (statusCode === 20101) {
// ...
}
}
處理通用處理函數的部分,這裡建立了 processPlayerData
函數,可以接受任何類型的玩家物件作為參數,目標是不直接依賴於特定的玩家類型讓函數更具通用性,方便未來新增更多的角色。
判斷的部份選擇把不同功能的判斷用 function 拆開,將這些條件拆分成獨立的函數可以提高程式碼的可讀性和維護性,這樣做讓每個檢查條件都有自己的名稱,更容易理解和測試。
// 定義一個函數,用於檢查玩家的健康狀態
function isHealthy(player) {
return player.health > 10;
}
// 定義一個函數,用於檢查玩家的名稱和傷害值
function hasDesiredNameOrLowDamage(player) {
return player.name === "foo" || player.damage < 5;
}
// 定義一個函數,用於檢查玩家的名稱和敏捷度
function hasDesiredNameOrHighAgility(player) {
return player.name !== "bar" || player.agility > 20;
}
// 重新設計函數,只接受角色物件作為參數
function processPlayerData(player) {
if (
isHealthy(player) &&
hasDesiredNameOrLowDamage(player) &&
hasDesiredNameOrHighAgility(player)
) {
// ...
// 這裡執行根據條件的操作
}
// 非常長的函數內容...
if (statusCode === 20100) {
// ...
}
if (statusCode === 20101) {
// ...
}
}
重構完之後發現,只有 AdvancedPlayer 才需要有 hasDesiredNameOrHighAgility,所以再把腳色拆分進行重構,讓每個玩家類型都分別具有自己的檢查邏輯,基本玩家和進階玩家分別知道如何檢查自己的條件,這就是介面隔離。
// 定義一個函數,用於檢查玩家的健康狀態
function isHealthy(player) {
return player.health > 10;
}
// 定義一個函數,用於檢查玩家的名稱和傷害值
function hasDesiredNameOrLowDamage(player) {
return player.name === "foo" || player.damage < 5;
}
// 定義一個函數,用於檢查玩家的名稱和敏捷度
function hasDesiredNameOrHighAgility(player) {
return player.name !== "bar" || player.agility > 20;
}
// 定義一個基本玩家函數,接受包含玩家屬性的物件作為參數
function createPlayer(playerData) {
const { name, health, damage } = playerData;
return {
name,
health,
damage,
// 通用的檢查函數,每個玩家都可以使用
checkPlayerCondition() {
return isHealthy(player) && hasDesiredNameOrLowDamage(player);
},
};
}
// 定義進階玩家函數,接受包含進階玩家屬性的物件作為參數
function createAdvancedPlayer(advancedPlayerData) {
const { name, health, damage, agility } = advancedPlayerData;
const player = createPlayer({ name, health, damage });
return {
...player,
agility,
// 進階玩家特有的檢查函數
checkAdvancedPlayerCondition() {
return hasDesiredNameOrHighAgility(player);
},
};
}
// 通用的處理玩家資料函數,接受任何類型的玩家物件作為參數
function processPlayerData(player) {
if (player.checkPlayerCondition()) {
// 做一些基本玩家的操作
console.log(`${player.name} 符合基本條件`);
}
if (
player?.checkAdvancedPlayerCondition &&
player?.checkAdvancedPlayerCondition()
) {
// 做一些進階玩家的操作
console.log(`${player.name} 符合進階條件`);
}
// 非常長的函數內容...
if (statusCode === 20100) {
// ...
}
if (statusCode === 20101) {
// ...
}
}
// 建立玩家物件,使用包含屬性的物件作為參數
const player1 = createPlayer({
name: "玩家1",
health: 100,
damage: 10,
});
const player2 = createAdvancedPlayer({
name: "玩家2",
health: 150,
damage: 15,
agility: 20,
});
// 處理玩家資料
processPlayerData(player1);
// 輸出:玩家1 符合基本條件
processPlayerData(player2);
// 輸出:玩家2 符合基本條件
// 輸出:玩家2 符合進階條件
processPlayerData
函數最後不需要知道你是哪種特定的玩家類型,只關心玩家是否符合 Player 介面,如果未來新增其他類型的玩家,我們只需透過 Player 介面建立一個新的玩家類別,而不需要修改現有的程式碼。
透過這樣的設計降低了不同角色之間的耦合性,這就是介面隔離原則 (Interface Segregation Principle),介面隔離原則有助於確保程式碼的結構清晰,並使不同部分之間的依賴關係簡化,同時支持未來的擴充和修改。